黑马点评Redis
实现短信的登录和注册
com/hmdp/controller/UserController.java
package com.hmdp.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.query;
/**
* <p>
* 前端控制器
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session);
}
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
String phone = loginForm.getPhone();
// 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
// 不一致 报错
if (cacheCode == null || !code.equals(cacheCode)) {
return Result.fail("验证码错误");
}
// 一致 根据手机号查询用户 用mybatisplus
User user = userService.getOne(lambdaQuery(User.class).eq(User::getPhone, phone));
// 判断用户是否存在
if (ObjectUtil.isEmpty(user)) {
// 不存在 创建新用户保存
user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
userService.save(user);
}
// 存在 保存用户信息到session当中
UserDTO userDTO = new UserDTO();
userDTO.setId(user.getId());
userDTO.setNickName(user.getNickName());
userDTO.setPhone(user.getPhone());
session.setAttribute("user", userDTO); // 确保保存的是 UserDTO 对象
return Result.ok();
}
/**
* 登出功能
* @return 无
*/
@PostMapping("/logout")
public Result logout(){
// TODO 实现登出功能
return Result.fail("功能未完成");
}
@GetMapping("/me")
public Result me(){
// TODO 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
@GetMapping("/info/{id}")
public Result info(@PathVariable("id") Long userId){
// 查询详情
UserInfo info = userInfoService.getById(userId);
if (info == null) {
// 没有详情,应该是第一次查看详情
return Result.ok();
}
info.setCreateTime(null);
info.setUpdateTime(null);
// 返回
return Result.ok(info);
}
}
com/hmdp/service/impl/UserServiceImpl.java
package com.hmdp.service.impl;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
// 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 不符合 返回错误信息
return Result.fail("手机号格式错误!");
}
// 符合 生成验证码
String code = RandomUtil.randomNumbers(6);
// 保存验证码到session
session.setAttribute("code", code);
// 发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
return null;
}
}
实现登录校验拦截器
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login"
);
}
}
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
package com.hmdp.utils;
import cn.hutool.core.util.ObjectUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session
HttpSession session = request.getSession();
// 获取session中的用户
Object user = session.getAttribute("user");
// 判断用户是否存在
if (ObjectUtil.isEmpty(user)) {
// 不存在,拦截
response.setStatus(401);
return false;
}
// 存在,保存用户信息到ThreadLocal
UserDTO userDTO = (UserDTO) user; // 确保 user 已经是 UserDTO 类型
UserHolder.saveUser(userDTO);
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
集群的Session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
session的替代方案应该满足:数据共享、内存存储、key、value结构
发送短信验证码后生成的验证码 保存验证码到Redis → 以手机号作为Key存储验证码 → 发送验证码
com/hmdp/controller/UserController.java
package com.hmdp.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery;
import static com.baomidou.mybatisplus.core.toolkit.Wrappers.query;
/**
* <p>
* 前端控制器
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
@Resource
private IUserInfoService userInfoService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session);
}
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
String phone = loginForm.getPhone();
// 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 校验验证码
// Object cacheCode = session.getAttribute("code");
// 从Redis中获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(SystemConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
// 不一致 报错
if (cacheCode == null || !code.equals(cacheCode)) {
return Result.fail("验证码错误");
}
// 存储
// 一致 根据手机号查询用户 用mybatisplus
User user = userService.getOne(lambdaQuery(User.class).eq(User::getPhone, phone));
// 判断用户是否存在
if (ObjectUtil.isEmpty(user)) {
// 不存在 创建新用户保存
user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
userService.save(user);
}
// 保存用户信息到redis中
// 随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 将User对象转换为Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 存储 用了putall 要把userDto转map
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 存储 因为存的时候不能设置有效期 要存完以后再去设置有效期
String tokenKey = SystemConstants.LOGIN_CODE_TOKEN + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 设置token有效期 30min
stringRedisTemplate.expire(tokenKey, SystemConstants.REDIS_TIMEOUT, TimeUnit.MINUTES);
// 但是我要如果状态在 就不断更新token的有效期 【更新token有效期】
// 存在 保存用户信息到session当中
// UserDTO userDTO = new UserDTO();
// userDTO.setId(user.getId());
// userDTO.setNickName(user.getNickName());
// userDTO.setPhone(user.getPhone());
// 确保保存的是 UserDTO 对象
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
/**
* 登出功能
* @return 无
*/
@PostMapping("/logout")
public Result logout(){
// TODO 实现登出功能
return Result.fail("功能未完成");
}
@GetMapping("/me")
public Result me(){
// TODO 获取当前登录的用户并返回
// userService.login(loginForm, session)
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
@GetMapping("/info/{id}")
public Result info(@PathVariable("id") Long userId){
// 查询详情
UserInfo info = userInfoService.getById(userId);
if (info == null) {
// 没有详情,应该是第一次查看详情
return Result.ok();
}
info.setCreateTime(null);
info.setUpdateTime(null);
// 返回
return Result.ok(info);
}
}
解决状态登录刷新的问题【双重拦截器】
请求 → 拦截器①(拦截一切路径)[流程:获取token、查询Redis用户、保存到ThreadLocal、刷新Token有效期、放行] → 拦截器②(拦截需要登录的路径)[查询ThreadLocal的用户, 不存在则拦截、存在则继续]
com/hmdp/utils/LoginInterceptor.java
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class LoginInterceptor implements HandlerInterceptor {
// 这个类的对象是手动new出来的 没有通过Spring容器 所以需要通过构造方法去引用
// 拦截器不能给spring容器因为每次都会经过它去访问
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,需要拦截,设置状态码
response.setStatus(401);
// 拦截
return false;
}
// 有用户 放行
return true;
}
}
com/hmdp/utils/RefreshTokenInterceptor.java
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class RefreshTokenInterceptor implements HandlerInterceptor {
// 这个类的对象是手动new出来的 没有通过Spring容器 所以需要通过构造方法去引用
// 拦截器不能给spring容器因为每次都会经过它去访问
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 不存在:拦截,返回401状态码
// response.setStatus(401);
return true;
}
String key = SystemConstants.LOGIN_CODE_TOKEN + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 判断用户是否存在
// if (ObjectUtil.isEmpty(user)) {
if (userMap.isEmpty()){
// 不存在,拦截
// response.setStatus(401);
return true;
}
// 将查询到的Hash数据转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 存在,保存用户信息到ThreadLocal
// UserDTO userDTO = (UserDTO) user; // 确保 user 已经是 UserDTO 类型
UserHolder.saveUser(userDTO);
// 刷新token有效期
stringRedisTemplate.expire(key, SystemConstants.REDIS_TIMEOUT, TimeUnit.MINUTES);
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
com/hmdp/config/MvcConfig.java
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
商户查询缓存—练习题
com/hmdp/controller/ShopTypeController.java
/**
* <p>
* 前端控制器
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {
@Resource
private IShopTypeService typeService;
@GetMapping("list")
public Result queryTypeList() {
// List<ShopType> typeList = typeService
// .query().orderByAsc("sort").list();
// return Result.ok(typeList);
return typeService.listShop();
}
com/hmdp/service/impl/ShopTypeServiceImpl.java
/**
* <p>
* 服务实现类
* </p>
* @since 2021-12-22
*/
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result listShop() {
String shopTypeCache = stringRedisTemplate.opsForValue().get(SystemConstants.CACHE_SHOP_TYPE_KEY);
if (ObjectUtil.isNotEmpty(shopTypeCache)) {
Long ttl = stringRedisTemplate.getExpire(SystemConstants.CACHE_SHOP_TYPE_KEY, TimeUnit.MINUTES);
System.out.println("TTL for CACHE_SHOP_TYPE_KEY: " + ttl + " minutes");
List<ShopType> shopTypeList = JSONUtil.toList(shopTypeCache, ShopType.class);
return Result.ok(shopTypeList);
}
List<ShopType> queryShopTypeList = query().orderByAsc("sort").list();
if (ObjectUtil.isEmpty(queryShopTypeList)) {
return Result.fail("查询失败");
}
stringRedisTemplate.opsForValue().set(SystemConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(queryShopTypeList), SystemConstants.SHOP_TYPE_TTL, TimeUnit.MINUTES);
return Result.ok(queryShopTypeList);
}
}
添加商铺缓存
com/hmdp/service/impl/ShopServiceImpl.java
package com.hmdp.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
// 从redis中查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(SystemConstants.CACHE_SHOP_KEY + id);
// 判断是否存在
if (ObjectUtil.isNotEmpty(shopJson)) {
// 存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 不存在,根据id查询数据库
Shop shop = getById(id);
// 不存在,返回错误
if (ObjectUtil.isEmpty(shop)) {
return Result.fail("店铺不存在");
}
// 存在,写入redis
stringRedisTemplate.opsForValue().set(SystemConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));
// 返回
return Result.ok(shop);
}
}
实现商铺缓存与数据库的双写一致性
先更新后删除
com/hmdp/controller/ShopController.java
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// shopService.updateById(shop);
return shopService.update(shop);
}
com/hmdp/service/impl/ShopServiceImpl.java
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if (ObjectUtil.isEmpty(id)) {
return Result.fail("店铺id不能为空");
}
// 更新数据库
updateById(shop);
// 删除缓存
stringRedisTemplate.delete(SystemConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
缓存穿透的解决思路
缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
缓存null的时候加一个TTL
布隆过滤器
算法[bitMap数组用Hash值去判断]- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能
缓存击穿的解决思路
缓存雪崩的解决思路
利用互斥锁解决缓存击穿问题setnx设置一把锁
setnx
当key不存在的时候才会写入赋值del lock
释放锁、setnx lock
获取锁赋值
在linux中如果用的docker那么就用它
docker连接redis
首先先找到redis的镜像编码docker ps
找到以后docker exec -it 5832be55766e redis-cli
[root@localhost ~]# docker exec -it 5832be55766e redis-cli
127.0.0.1:6379> AUTH pass
OK
127.0.0.1:6379> setnx lock 1
(integer) 1
127.0.0.1:6379> get lock
"1"
127.0.0.1:6379> setnx lock 2
(integer) 0
127.0.0.1:6379> setnx lock 3
(integer) 0
127.0.0.1:6379> setnx lock 2
(integer) 0
127.0.0.1:6379> get lock
"1"
127.0.0.1:6379> del lock
(integer) 1
127.0.0.1:6379> setnx lock 3
(integer) 1
--------------------------------------------------------------------------
// 加个有效期TTL 避免锁得不到释放 产生死锁
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
// 防止自动拆箱产生空值
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
SETNX
是 Redis 的一个命令,它是 “SET if Not eXists” 的缩写,用于在键不存在时设置键的值。如果键已经存在,SETNX
命令不会执行任何操作,并返回0
;如果键不存在,它会设置键的值,并返回1
。在分布式锁的实现中,
SETNX
命令经常被用来尝试获取锁。如果某个键(通常用作锁的标识)不存在,则设置该键并获取锁;如果键已经存在,表示锁已经被其他客户端持有,因此当前客户端无法获取锁。以下是
SETNX
命令的基本用法:SETNX key value
key
:你想要设置的键名。value
:你想要设置的值。从 Redis 2.6.12 版本开始,
SETNX
命令被SET
命令的一个选项所取代,该选项允许你执行类似于SETNX
的操作,同时还能设置键的超时时间(过期时间),这在实现分布式锁时非常有用。以下是使用SET
命令的示例:SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds
:设置键的过期时间为 seconds 秒。PX milliseconds
:设置键的过期时间为 milliseconds 毫秒。NX
:只在键不存在时执行设置操作。XX
:只在键已经存在时执行设置操作。因此,现代的分布式锁实现通常会使用如下命令:
SET lock_key unique_value EX 30 NX
这里,
lock_key
是锁的键名,unique_value
是一个确保释放锁时安全的唯一值(通常是当前时间戳加上一个随机数或者客户端ID),EX 30
设置了键的超时时间为30秒,NX
确保只有在键不存在时才设置键。互斥锁的实现
互斥锁通常用于单机应用,确保同一时刻只有一个线程可以执行某个操作。以下是一个简单的示例:
java
复制
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; public class MutexLock { private final StringRedisTemplate redisTemplate; private final String lockKey = "mutex_lock_key"; public MutexLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public boolean tryLock() { ValueOperations<String, String> ops = redisTemplate.opsForValue(); // 尝试设置锁,如果设置成功返回true,否则返回false return ops.setIfAbsent(lockKey, "locked"); } public void unlock() { // 删除锁 redisTemplate.delete(lockKey); } }
分布式锁的实现
分布式锁用于分布式系统,确保在多个不同的机器或服务中只有一个可以执行某个操作。以下是一个简单的示例:
java
复制
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; public class DistributedLock { private final StringRedisTemplate redisTemplate; private final String lockKey = "distributed_lock_key"; private final long lockTimeout = 30000; // 锁超时时间,例如30秒 public DistributedLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public boolean tryLock() { ValueOperations<String, String> ops = redisTemplate.opsForValue(); // 尝试设置锁,并设置超时时间 Boolean success = ops.setIfAbsent(lockKey, "locked", lockTimeout, TimeUnit.MILLISECONDS); return Boolean.TRUE.equals(success); } public void unlock() { // 删除锁 redisTemplate.delete(lockKey); } }
基于逻辑过期方式解决缓存击穿的问题
不要直接在类中添加逻辑过期的字段,这样对代码不好
① 搞一个RedisData
.java 设置一个逻辑过期 然后再去实现继承
② RedisData中搞一个private Object data;
热点数据需要提前导入
封装Redis工具类
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击
穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
Redis总结
优惠券秒杀—全局唯一ID
全局唯一ID生成策略:
- UUID
- Redis自增
- snowflake算法
- 数据库自增
Redis自增ID策略:
- 每天一个Key,方便统计订单量
- ID构造是 时间戳 + 计数器
添加优惠券秒杀针对特价券
每个店铺都可以发布优惠券,分为平价券和特价券。
平价券可以任意购买,而特价券需要秒杀抢购:
tb_voucher
:优惠券的基本信息,优惠金额、使用规则等tb_seckill_voucher
:优惠券的库存、开始抢购时间、结束抢购时间。特价优惠券才需要填写这些信息
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
03154afad287 seataio/seata-server:1.4.2 "java -Djava.securit…" 11 months ago Up About an hour 8091/tcp, 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp seata-server
c34a590a5649 xuxueli/xxl-job-admin:2.1.2 "sh -c 'java -jar /a…" 11 months ago Up About an hour 0.0.0.0:8280->8080/tcp, :::8280->8080/tcp xxl-job-admin
3574b316b5d2 nacos/nacos-server:1.4.0 "bin/docker-startup.…" 11 months ago Up About an hour 0.0.0.0:8848->8848/tcp, :::8848->8848/tcp nacos
951e83eb8120 rabbitmq:3.8.3-management "docker-entrypoint.s…" 11 months ago Up About an hour 4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp rabbitmq
6561e8458fa2 influxdb:1.8.0 "/entrypoint.sh infl…" 11 months ago Up About an hour 0.0.0.0:8086->8086/tcp, :::8086->8086/tcp, 0.0.0.0:8088->8088/tcp, :::8088->8088/tcp, 0.0.0.0:9083->8083/tcp, :::9083->8083/tcp influxdb
5832be55766e redis:5.0.0 "docker-entrypoint.s…" 11 months ago Up About an hour 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp redis
07ecd8b04853 mysql:5.7 "docker-entrypoint.s…" 11 months ago Up About an hour 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql
[root@localhost ~]# docker exec -it 5832be55766e redis-cli
127.0.0.1:6379> AUTH pass

库存超卖问题分析每秒上百上千的并发
超卖问题是经典的多线程安全问题,针对这一问题的常见解决方案就是加锁:
秒杀 → 一人一单拒绝黄牛
要求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
集群下线程的并发安全问题
分布式锁 — 基本原理
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用mysql本身的互斥锁机制 | 利用setnx这样的互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
基于Redis的分布式锁
实现分布式锁时需要实现的两个基本方法:获取锁、释放锁
改进Redis的分布式锁
Redis的事务:可以使用看门狗
Redis的Lua脚本利用Lua去调用Redis(确保原子的一致性)
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
redis.call('命令名称', 'key', '其他参数', ...)
例如我们要执行set name jack,则脚本是这样:redis.call('set', 'name', 'jack')
例如我们要先执行set name Rose,再执行get name,则脚本如下:
# 先执行 set name jack
redis.call('set', 'name', 'jack')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name
执行Lua脚本
写好脚本后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
127.0.0.1:6379> help @scripting
EVAL script numkeys key [key ...] arg [arg ...]
要执行redis.call('set', 'name', 'jack')
这个脚本,语法如下:
# 调用脚本 [0是脚本需要的key类型的参数个数 => 传参的变量]
EVAL "return redis.call('set', 'name', 'jack')" 0
# 不带变量
127.0.0.1:6379> EVAL "return redis.call('set', 'name', 'Jack')" 0
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"Jack"
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:【lua语言数组脚标是从1开始的】
# 调用脚本【初始版】
127.0.0.1:6379> EVAL "return redis.call('set', 'name', 'Jack')" 0
# 调用脚本【进阶版】
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose
OK
127.0.0.1:6379> get name
"Rose"
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name heihei
OK
127.0.0.1:6379> get name
"heihei"
基于Redis的分布式锁
释放锁的业务流程是这样的:
1.获取锁中的线程标示
2.判断是否与指定的标示 (当前线程标示) 一致
3.如果一致则释放锁 (删除)
4.如果不一致则什么都不做
-- 锁的key
-- local key = "lock:order:5"
-- 不能写死就传参
local threadId = KEYS[1]
-- 当前线程标示
-- local threadId = "fagsidajkldw-33"
-- 不能写死就传参
local threadId = ARGV[1]
-- 获取锁中的线程标识 get key
local id = redis.call('get', key)
-- 比较线程标示与锁中的标示是否一致
if(id == threadId) then
-- 释放锁 del key
return redis.call('del', key)
end
return 0
----------------------------- 进阶版 -------------------------------
-- 获取锁中的线程标识 get key
local id = redis.call('get', KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
----------------------------- 进阶简化版 -------------------------------
-- 这里KEYS[1]就是锁的Key, 这里的ARGV[1],就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if(redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致 直接返回
return 0
再次改进Redis的分布式锁经典白雪
基于Redis的分布式锁实现思路:
利用setnx ex获取锁,并设置过期时间,保存线程标示
释放锁时先判断线程标示是否与自己一致,一致则删除锁特性:
利用set nx满足互斥性
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
利用Redis集群保证高可用和高并发特性
需求:基于**Lua脚本
**实现分布式锁的释放锁逻辑
提示:RedisTemplate
调用Lua脚本的API如下:
com/hmdp/utils/SimpleRedisLock.java
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock {
// 不同业务不同锁
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// 是静态的要在静态代码块里面做初始化 这个类一加载 这个代码块就初始化完成了 不用每次释放锁再加载
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(String name,
StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name,
threadId,
timeoutSec,
TimeUnit.SECONDS);
// 自动拆箱避免空指针风险
return Boolean.TRUE.equals(success);
}
@Override
public void unLock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
// 制造单集合
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
// public void unLock() {
// // 获取线程标识
// String threadId = ID_PREFIX + Thread.currentThread().getId();
// // 获取锁
// String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// // 判断锁是否ours
// if (threadId.equals(id)) {
// // 解锁
// stringRedisTemplate.delete(KEY_PREFIX + name);
// }
// }
}
基于Redis的分布式锁优化
基于setnx实现的分布式锁存在下面的问题:
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
- 主从一致性:如果Redis提供了主从集群 [写操作访问主节点,读操作访问从节点],主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
Redisson
Redisson是一个在Redis的基础上实现的]ava驻内存数据网格(In-MemoryData Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
Redisson—官方网站
Redisson—GitHub地址
Redisson可重入锁原理
package com.hmdp;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Slf4j
@SpringBootTest
class RedissonTest {
@Resource
private RedissonClient redissonClient;
private RLock lock;
@BeforeEach
void setUp() {
lock = redissonClient.getLock("order");
}
@Test
void method1() throws InterruptedException {
// 尝试获取锁
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
if (!isLock) {
log.error("获取锁失败 .... 1");
return;
}
try {
log.info("获取锁成功 .... 1");
method2();
log.info("开始执行业务 ... 1");
} finally {
log.warn("准备释放锁 .... 1");
lock.unlock();
}
}
void method2() {
// 尝试获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败 .... 2");
return;
}
try {
log.info("获取锁成功 .... 2");
log.info("开始执行业务 ... 2");
} finally {
log.warn("准备释放锁 .... 2");
lock.unlock();
}
}
}
Redisson的锁重试和WatchDog机制
基于Redis的分布式锁优化
基于setnx实现的分布式锁存在下面的问题:
不可重入:同一个线程无法多次获得同一把锁
不可重试:获取锁只尝试一次就返回false,没有重试机制
超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
主从一致性:如果Redis提供了主从集群主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现